//*********************************************************
//
//    Copyright (c) Microsoft. All rights reserved.
//    This code is licensed under the Apache License, Version 2.0.
//    THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF
//    ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY
//    IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR
//    PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.
//
//*********************************************************

namespace Trident.Scheduler.Server
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using Microsoft.Research.eResearch.TridentScheduleCommon;
    using ER = Microsoft.Research.DataLayer;
    using System.IO;
    using System.Reflection;
    using System.Diagnostics;
    using System.Collections.ObjectModel;
    using Microsoft.Research.DataLayer;
    using Microsoft.Research.eResearch.Common.Logging;
    using Microsoft.Research.eResearch.Common.Eventing;
    using System.Globalization;
    using System.Management;

    public class TridentServerScheduler : ITridentScheduler, ITridentScheduleStatus
    {
        #region private members
        /// <summary>
        /// Registry conneciton handle
        /// </summary>
        private ER.Connection registryConnection = null;

        /// <summary>
        /// Workflow launching process name
        /// </summary>
        private string WFProcessName;

        /// <summary>
        /// Exit process event handler
        /// </summary>
        private EventHandler processExit;

        /// <summary>
        /// Workflow launched event handler delegate
        /// </summary>
        public event EventHandler<WFLauchEventArgs> WorkflowUIHandler;

        /// <summary>
        /// Schedule engine object
        /// </summary>
        private List<ITridentServerScheduleEngine> customScheduleEngines = null;

        /// <summary>
        /// Processing jobs list
        /// </summary>
        private Dictionary<Guid, int> processingJobs = new Dictionary<Guid, int>();
        /// <summary>
        /// Success exit code
        /// </summary>
        private const int exitSuccess = 0;

        #endregion

        /// <summary>
        /// This function is used to check and 
        /// reopen the registry connection if it is not active.
        /// </summary>
        private void ReOpenRegistryConnection()
        {
            if (null == this.registryConnection || !this.registryConnection.Alive)
            {
                ER.ConnectionManager mgr = ER.ConnectionManager.CreateForAgent(
                                                            ER.ConnectionManager.CancelBehavior.ThrowException,
                                                            ER.ConnectionManager.Agent.ExecutionService);
                this.registryConnection = mgr.PickConnection(ER.ConnectionManager.ConnectionUI.NeverShowUI);
            }
        }

        /// <summary>
        /// Method will load all the running instances of wfconsoleapp.exe process to give context 
        /// </summary>
        private void PopulateRunningJobsDictionary()
        {
            try
            {
                string wmiQuery = string.Format("select CommandLine, ProcessId from Win32_Process where Name='{0}'", Path.GetFileName(WFProcessName));
                ManagementObjectSearcher processSearcher = new ManagementObjectSearcher(wmiQuery);
                ManagementObjectCollection wfConsoleCollection = processSearcher.Get();
                foreach (ManagementObject wfConsoleObject in wfConsoleCollection)
                {
                    string commandLine = wfConsoleObject["CommandLine"] as string;
                    if (!String.IsNullOrEmpty(commandLine))
                    {
                        string processID = wfConsoleObject["ProcessId"].ToString();
                        string[] strArray = commandLine.Split(" ".ToCharArray());
                        string jobID = strArray[strArray.Length - 1];
                        if (!string.IsNullOrEmpty(processID))
                        {
                            int pID = Convert.ToInt32(processID);
                            Process process = Process.GetProcessById(pID);
                            if (null != process)
                            {
                                process.Exited += new EventHandler(wfProcess_Exited);
                                processingJobs.Add(new Guid(jobID), pID);
                            }
                        }
                    }
                }
            }
            catch (Exception exp)
            {
                this.Log("Error", exp.Message);
                this.Log("Error", exp.StackTrace);
            }
        }

        #region ITridentScheduler Members
        /// <summary>
        /// This ITridentScheduler interface implementation initializes the scheduler
        /// </summary>
        /// <param name="executionProcName"></param>
        /// <param name="showConsoleProc"></param>
        /// <param name="registryConn"></param>
        /// <param name="exitEvent"></param>
        /// <param name="launchWorkflowUIHandler"></param>
        public void Initialize(string executionProcName,
                                ER.Connection registryConn,
                                EventHandler exitEvent,
                                EventHandler<WFLauchEventArgs> launchWorkflowUIHandler)
        {
            this.WFProcessName = executionProcName;
            registryConnection = registryConn;
            processExit = exitEvent;
            WorkflowUIHandler = launchWorkflowUIHandler;
            customScheduleEngines = new List<ITridentServerScheduleEngine>();
            //Load the custom logic engines if any
            LoadEngine();

            //Populate all the running wf console app instances
            PopulateRunningJobsDictionary();
        }

        /// <summary>
        /// Loads the schedule engines, if configured in the same folder
        /// No engine configured could be a common scenario for server version 
        /// Application will not try to reload engines during every ProcessJobs call
        /// </summary>
        private void LoadEngine()
        {
            DirectoryInfo dirInfo = new DirectoryInfo(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location));
            //Enumerate the engine dlls available in the system
            FileInfo[] fileinfos = dirInfo.GetFiles("Trident.Server.Schedule.Engine*.dll");
            foreach (FileInfo info in fileinfos)
            {
                //Load the assembly and try to find the object exposing the engine interface
                Assembly asm = Assembly.LoadFile(fileinfos[0].FullName);
                Type[] asmTypes = asm.GetTypes();
                foreach (Type t in asmTypes)
                {
                    Type[] ifaces = t.GetInterfaces();
                    if (null != ifaces && ifaces.Contains(typeof(ITridentServerScheduleEngine)))
                    {
                        //Found the class which has Schedule engine implementation, create an object and keep it
                        ITridentServerScheduleEngine engine = (ITridentServerScheduleEngine)Activator.CreateInstance(t);
                        if (null != engine)
                        {
                            //Initialize the engine if we get one
                            engine.Initialize();
                            customScheduleEngines.Add(engine);
                        }
                        break;
                    }
                }
            }
        }

        /// <summary>
        /// This method should be called for processing the list of jobs
        /// Will reroute each job to the configured schedule engine
        /// If engine returns false job will mark it as aborted 
        /// If engine returns true job will be processed normally
        /// </summary>
        /// <param name="jobs"></param>
        public void ProcessJobs(bool showConsole, Collection<ER.Job> jobs, ER.Machine myMachine)
        {
            try
            {
                //Connect to registry, if not connected already
                ReOpenRegistryConnection();
                //Loop each job and try customization and schedule
                foreach (ER.Job job in jobs)
                {
                    bool continueSchedule = true;
                    //Execute the custom code to change the scheduling 
                    foreach (ITridentServerScheduleEngine engine in customScheduleEngines)
                    {
                        try
                        {
                            //If any of the schedule engine returns false, particular job will be marked as terminated. 
                            if (false == engine.CustomizeJob(job, myMachine))
                            {
                                job.Status = ER.JobStatus.Aborted;
                                job.ErrorMessage = @"Terminated by the Server Schedule Engine rule";
                                this.Log("Error", "{0} {1}", ((ER.IObject)job).ID.ToString(), job.ErrorMessage);
                                continueSchedule = false;
                                break;
                            }
                        }
                        catch (Exception exp)
                        {
                            this.Log("Error", "Customization thrown an exception, discarding it and proceeding with next engine.");
                            this.Log("Error", exp.Message);
                        }
                    }
                    //Check whether job is already aborted by any engine otherwise proceed
                    if (continueSchedule)
                    {
                        job.Status = ER.JobStatus.StartPending;

                        // Create the WF Processes
                        ProcessStartInfo pi = new ProcessStartInfo();
                        pi.FileName = this.WFProcessName;
                        pi.Arguments = "-j " + ((ER.IObject)job).ID.ToString();
                        pi.WindowStyle = showConsole ? ProcessWindowStyle.Normal : ProcessWindowStyle.Hidden;
                        Process wfProcess = new Process();
                        wfProcess.StartInfo = pi;

                        if (wfProcess.Start())
                        {
                            //Add the process to dictionary for future reference
                            processingJobs.Add(((ER.IObject)job).ID, wfProcess.Id);

                            wfProcess.EnableRaisingEvents = true;
                            wfProcess.Exited += new EventHandler(wfProcess_Exited);

                            //Invoke the ui handler to notify the job scheduled
                            if (this.WorkflowUIHandler != null)
                            {
                                WorkflowUIHandler.Invoke(this, new WFLauchEventArgs(job.Name));
                            }
                        }
                    }
                }
            }
            catch (Exception exp)
            {
                this.Log("Error", exp.Message);
            }
        }
        /// <summary>
        /// Event handler for console process exit to change the status
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void wfProcess_Exited(object sender, EventArgs e)
        {
            Connection registryConn = null;
            try
            {
                //Open the registry connection if 
                ReOpenRegistryConnection();

                Process currProcess = sender as Process;
                if (null != currProcess)
                {
                    ProcessStartInfo pi = currProcess.StartInfo;
                    // Get the job id from arguments of the Process.
                    string jobID = pi.Arguments.Split(" ".ToCharArray())[1];
                    if (exitSuccess != currProcess.ExitCode)
                    {
                        registryConn = registryConnection.Clone();
                        registryConn.Open();

                        Job currentJob = Job.Load(new Guid(jobID), registryConn);
                        if (currentJob.Status == JobStatus.Running ||
                            currentJob.Status == JobStatus.StartPending)
                        {
                            if (string.IsNullOrEmpty(currentJob.ErrorMessage))
                            {
                                currentJob.ErrorMessage = @"Workflow runtime process exited";
                            }
                            currentJob.Status = JobStatus.Aborted;
                            processingJobs.Remove(new Guid(jobID));
                        }
                    }
                }
            }
            catch (Exception exp)
            {
                this.Log("Error", exp.Message);
            }
            finally
            {
                if (null != processExit)
                    this.processExit(sender, e);

                if (null != registryConn && registryConn.Alive)
                {
                    registryConn.Close();
                }
            }
        }
        #endregion

        #region ILoggable Members

        /// <summary>
        /// Fired when a log message is received.
        /// </summary>
        public event EventHandler<LogEventArgs> LogMessageReceived;

        /// <summary>
        /// Raises the LogMessageReceived event.
        /// </summary>
        /// <param name="level">The log level.</param>
        /// <param name="message">The log message.</param>
        protected virtual void OnLogMessageReceived(string level, string message)
        {
            this.LogMessageReceived.Fire(this, new LogEventArgs(level, message));
        }

        /// <summary>
        /// Writes the log message.
        /// </summary>
        /// <param name="message">Message string to be logged.</param>
        private void Log(string level, string message)
        {
            this.OnLogMessageReceived(level, String.Format(CultureInfo.CurrentUICulture, "ServerScheduler: {0}", message));
        }

        /// <summary>
        /// Writes the log message.
        /// </summary>
        /// <param name="format">Format of the message.</param>
        /// <param name="args">Arguments to be formatted.</param>
        private void Log(string level, string format, params object[] args)
        {
            this.Log(level, String.Format(CultureInfo.CurrentUICulture, format, args));
        }

        #endregion

        #region IScheduleStatusUpdate Members
        /// <summary>
        /// Checks the pending jobs list kept in dictionary and returns the corresponding status
        /// </summary>
        /// <param name="job"></param>
        /// <returns></returns>
        public JobStatus GetScheduleStatus(Job job)
        {
            JobStatus status = job.Status;
            if (processingJobs.ContainsKey(((ER.IObject)job).ID))
            {
                int pID = processingJobs[((ER.IObject)job).ID];
                try
                {
                    Process currentProcess = Process.GetProcessById(pID);
                    if (currentProcess.HasExited)
                    {
                        if (currentProcess.ExitCode != exitSuccess)
                        {
                            status = JobStatus.Aborted;
                        }
                        processingJobs.Remove(((ER.IObject)job).ID);
                    }
                }
                catch (ArgumentException exp)
                {
                    this.Log("Warning", @"Process already exited");
                    this.Log("Warning", exp.Message);
                    status = JobStatus.Aborted;
                    processingJobs.Remove(((ER.IObject)job).ID);
                }
            }
            else
            {
                //Refresh and check whether it got updated during the time we reach here
                job.Refresh();
                //Job status is still 
                if (job.Status == JobStatus.Running ||
                    job.Status == JobStatus.StartPending)
                {
                    status = JobStatus.Aborted;
                }
            }
            return status;
        }

        #endregion

        #region ITridentScheduler Members


        public void UpdateRegistryConnection(Connection registryConn)
        {
            this.registryConnection = registryConn;
        }

        #endregion
    }
}
